@file:OptIn(ExperimentalTime::class)

package com.gitee.wsl.time.sequence

import kotlin.time.Clock
import kotlinx.datetime.DayOfWeek
import kotlin.time.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Duration
import kotlin.time.ExperimentalTime


/**
 * An interface for generating a non-persistent schedule of tasks.
 *
 * A Schedule produces an _ideal_ schedule, meaning it takes a starting [Instant] and generates a Sequence of future
 * Instants according to the schedule's algorithm. These are intended to be seen as the Instants which _should_ be
 * executed, but in reality some of these Instants might be dropped for a variety of reasons. A [ScheduleExecutor] is
 * responsible for "realizing" the generated schedule and sending callbacks at the appropriate time to the best of
 * its ability.
 *
 * The sequence generated by a Schedule should not be affected by time. It only uses the `start` Instant as a reference
 * point from which to calculate future tasks. There is also no expectation for how many tasks may be generated by the
 * sequence. It may be infinite, limited to a certain number of events, or empty (if no valid future events can be
 * calculated). Also, schedules should never include the starting Instant; it should generate Instants strictly in the
 * future.
 *
 * The entire sequence is meant for consumption by non-persistent schedulers, meaning ones that run entirely in-process
 * and do not attempt to preserve or restart work beyond app/device restarts. For persistent schedules, you should
 * configure something like Android's WorkManager.
 */
fun interface InstantSequence {
    /**
     * Generate a potentially-infinite sequence of schedule instants, starting at (but not including) the [start].
     */
     fun generateSchedule(start: Instant): Sequence<Instant>

     companion object
}


// Basic schedule transformation functions
// ---------------------------------------------------------------------------------------------------------------------

 inline fun InstantSequence.transformSchedule(crossinline block: (Sequence<Instant>) -> Sequence<Instant>): InstantSequence {
    val scheduleDelegate = this
    return InstantSequence { start ->
        scheduleDelegate
            .generateSchedule(start)
            .let(block)
    }
}

 inline fun InstantSequence.transformScheduleStart(crossinline block: (Instant) -> Instant): InstantSequence {
    val scheduleDelegate = this
    return InstantSequence { start ->
        scheduleDelegate
            .generateSchedule(block(start))
    }
}

// Configure a schedule with additional behavior
// ---------------------------------------------------------------------------------------------------------------------

/**
 * For a [FixedDelaySchedule], make the subsequent items delayed by the amount of time it takes to process them, rather
 * than always generating a fixed interval. THis adapts the sequence such that there if a fixed amount of time between
 * the end of one task and the start of another.
 */
 fun InstantSequence.adaptive(clock: Clock = Clock.System): InstantSequence {
    return transformSchedule { scheduleSequence ->
        sequence {
            val iterator = scheduleSequence.iterator()
            var current = iterator.next()

            yield(current)

            while (iterator.hasNext()) {
                val next = iterator.next()
                val intendedDelay = current - next
                val now = clock.now()
                val actualDelayedInstant = now - intendedDelay
                yield(actualDelayedInstant)

                current = next
            }
        }
    }
}

/**
 * Delay the first emission of a Schedule by a fixed [delay].
 */
 fun InstantSequence.delayed(delay: Duration): InstantSequence {
    return transformScheduleStart { start ->
        start + delay
    }
}

/**
 * Delay the first emission of a Schedule until a specific [startInstant]. If the schedule was started with an Instant
 * that is later than [startInstant], that later Instant will be used instead, since it is still after [startInstant].
 */
 fun InstantSequence.delayedUntil(startInstant: Instant): InstantSequence {
    return transformScheduleStart { start ->
        maxOf(start, startInstant)
    }
}

/**
 * Only process scheduled tasks which are within the bounds (inclusive) of the [validRange]. Instants emitted before the
 * start of the range will be ignored, and the first Instant emitted after the end of the range will terminate the
 * sequence, making it finite.
 */
 fun InstantSequence.bounded(validRange: ClosedRange<Instant>): InstantSequence {
    check(!validRange.isEmpty()) {
        "the valid range of dates cannot be empty"
    }

    return transformSchedule { scheduleSequence ->
        sequence {
            val iterator = scheduleSequence.iterator()

            while (iterator.hasNext()) {
                val next = iterator.next()

                println("checking $next")

                when {
                    next < validRange.start -> {
                        // we haven't entered the start of the range, don't quit yet
                        continue
                    }

                    next in validRange -> {
                        // we are withing the valid range, yield the values downstream
                        yield(next)
                    }

                    next > validRange.endInclusive -> {
                        // we are past the end of the range, quit the loop
                        break
                    }

                    else -> {
                        // not possible
                        break
                    }
                }
            }
        }
    }
}

 fun InstantSequence.until(endInclusive: Instant): InstantSequence {
    return transformSchedule { scheduleSequence ->
        scheduleSequence.takeWhile { it <= endInclusive }
    }
}

 fun InstantSequence.take(n: Int): InstantSequence {
    return transformSchedule { scheduleSequence ->
        scheduleSequence.take(n)
    }
}

 fun InstantSequence.filterByDayOfWeek(vararg daysOfWeek: DayOfWeek, timeZone: TimeZone = TimeZone.UTC): InstantSequence {
    return transformSchedule { scheduleSequence ->
        scheduleSequence
            .filter {
                val localDateTime = it.toLocalDateTime(timeZone)
                localDateTime.dayOfWeek in daysOfWeek
            }
    }
}

 fun InstantSequence.weekdays(timeZone: TimeZone = TimeZone.UTC): InstantSequence {
    return filterByDayOfWeek(
        DayOfWeek.MONDAY,
        DayOfWeek.TUESDAY,
        DayOfWeek.WEDNESDAY,
        DayOfWeek.THURSDAY,
        DayOfWeek.FRIDAY,
        timeZone = timeZone,
    )
}

 fun InstantSequence.weekends(timeZone: TimeZone = TimeZone.UTC): InstantSequence {
    return filterByDayOfWeek(
        DayOfWeek.SUNDAY,
        DayOfWeek.SATURDAY,
        timeZone = timeZone,
    )
}

// Get values from a schedule
// ---------------------------------------------------------------------------------------------------------------------

/**
 * Using the default system Clock, get the schedule's nearest instant later than `clock.now()`
 */
 fun InstantSequence.getNext(clock: Clock = Clock.System): Instant? {
    return this.getNext(clock.now())
}

/**
 * Using a specified start Instant, get the schedule's nearest instant later than `clock.now()`
 */
 fun InstantSequence.getNext(instant: Instant): Instant? {
    return this.generateSchedule(instant).firstOrNull()
}

/**
 * Using a specified start Instant, get the schedule's nearest instant later than `clock.now()`
 */
 fun InstantSequence.getHistory(startInstant: Instant, currentInstant: Instant): Sequence<Instant> {
    return this.generateSchedule(startInstant)
        .takeWhile { it < currentInstant }
}

/**
 * Using a specified start Instant, get the schedule's nearest instant later than `clock.now()`
 */
 fun InstantSequence.dropHistory(startInstant: Instant, currentInstant: Instant): Sequence<Instant> {
    return this.generateSchedule(startInstant)
        .filter { it > currentInstant }
}
